package birdsong.server;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

import birdsong.comm.ServerComm;
import birdsong.comm.ServerCommunicator;
import birdsong.comm.ServerSideMessage;

public class BirdsongServerImplemented implements BirdsongServer {
	/**
	 * LinkedList of all the connected users (Clients)
	 */
	private LinkedList<Client> onlineClientsList = new LinkedList<Client>();
	private ServerComm serverComm;

	// STANDARD ERROR MESSAGES
	public static final String ERROR_CLIENT_OFFLINE = "ERROR - Client offline";
	public static final String ERROR_FOLLOWER_D_OFFLINE = "ERROR - Follower(d) offline";
	public static final String ERROR_PAYLOAD_NULL = "ERROR - Payload is null";
	public static final String ERROR_SERVERCOMM_NULL = "ERROR - serverComm is null";

	public static final String ERROR_MESSAGE_NULL = "ERROR - serverComm.getNextMessage() returns null";
	public static final String ERROR_DUPLICATED_NICKNAME = "";

	private ArrayList<String> errorLog = new ArrayList<String>();
	private int numberOfErrors = 0;

	public void start(ServerComm serverComm) {
		numberOfErrors = 0;
		errorLog.clear();
		this.serverComm = serverComm;

		// COMMENT
		boolean stopped = false;
		if (serverComm != null) {
			serverComm.start();
			while (!stopped) {

				// Getting the message, if any
				ServerSideMessage message = this.serverComm.getNextMessage();
				if (message != null) {
					// BIRDSONG_MESSAGE
					if (message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE) {
						try {
							processBirdSongMessage(message);

						} catch (IllegalArgumentException ex) {
							if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
								System.out.println(ERROR_CLIENT_OFFLINE);
							} else {
								if (ex.getMessage().equals(ERROR_PAYLOAD_NULL)) {
									serverComm.sendError(
											message.getClientNickname(),
											ERROR_PAYLOAD_NULL);
								}
							}
							errorLog.add(ex.getMessage());
							numberOfErrors++;
						}
					}

					// CLIENT_CONNECTED
					else if (message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED) {
						processClientConnectedMessage(message);
					}

					// CLIENT_DISCONNECTED
					else if (message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED) {
						try {
							processClientDisconnectedMessage(message);

						} catch (IllegalArgumentException ex) {
							if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
								errorLog.add(ERROR_CLIENT_OFFLINE);
								numberOfErrors++;
							}
						}

					}

					// FOLLOW
					else if (message.getType() == ServerSideMessage.Type.FOLLOW) {
						try {
							processFollowMessage(message);

						} catch (IllegalArgumentException ex) {
							if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
								System.out.println(ERROR_CLIENT_OFFLINE);
							} else {
								if (ex.getMessage().equals(ERROR_PAYLOAD_NULL)) {
									serverComm.sendError(
											message.getClientNickname(),
											ERROR_PAYLOAD_NULL);
								} else {
									if (ex.getMessage().equals(
											ERROR_FOLLOWER_D_OFFLINE)) {
										serverComm.sendError(
												message.getClientNickname(),
												ERROR_FOLLOWER_D_OFFLINE);
									}
								}
							}
							errorLog.add(ex.getMessage());
							numberOfErrors++;
						}
					}

					// UNFOLLOW
					else if (message.getType() == ServerSideMessage.Type.UNFOLLOW) {
						try {
							processUnfollowMessage(message);

						} catch (IllegalArgumentException ex) {
							if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
								/*
								 * The Network communications service shouldn't
								 * allow to send a message without a sender. But
								 * the service shouldn't go down because of it
								 */
								System.out.println(ERROR_CLIENT_OFFLINE);
							} else {
								if (ex.getMessage().equals(ERROR_PAYLOAD_NULL)) {
									serverComm.sendError(
											message.getClientNickname(),
											ERROR_PAYLOAD_NULL);
								} else {
									if (ex.getMessage().equals(
											ERROR_FOLLOWER_D_OFFLINE)) {
										serverComm.sendError(
												message.getClientNickname(),
												ERROR_FOLLOWER_D_OFFLINE);
									}
								}
							}
							errorLog.add(ex.getMessage());
							numberOfErrors++;
						}
					}
				} else {
					errorLog.add(ERROR_MESSAGE_NULL);
					numberOfErrors++;
					stopped = true;
				}
			}
		} else {
			errorLog.add(ERROR_SERVERCOMM_NULL);
			numberOfErrors++;
			throw new IllegalArgumentException(ERROR_SERVERCOMM_NULL);
		}
	}

	/**
	 * Updates the relationship between the asker and the followed, that is,
	 * asker unfollows the given client
	 * 
	 * @param message
	 *            The message to be processed (must be from UNFOLLOW type and
	 *            must have a payload)
	 * @throws IllegalArgumentException
	 *             Throws an exception whenever: The message hasn't a payload;
	 *             The client isn't online; The desired followed isn't online;
	 */
	private void processUnfollowMessage(ServerSideMessage message)
			throws IllegalArgumentException {
		// Checking the preconditions
		assert message.getType() == ServerSideMessage.Type.UNFOLLOW;

		// The client is asking to unfollow the user on the payload
		String askerNickname = message.getClientNickname();
		String followedNickname = message.getPayload();
		// Checking if payload has text
		if (message.getPayload() != null) {
			Client asker = getClientByNickname(askerNickname);
			// Checking if the asker is online - Precondition
			if (asker != null) {
				Client followed = getClientByNickname(followedNickname);
				// Checking if the choosen client to follow is online
				if (followed != null) {
					asker.removeFollowed(followed);
					followed.removeFollower(asker);

					// Warn the followed client about what happened;

					serverComm.sendNewUnfollow(followedNickname, askerNickname);
				} else {
					throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
				}
			} else {
				throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
			}
		} else {
			throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
		}
	}

	/**
	 * Updates the relationship between the asker and the followed, that is,
	 * asker unfollows the given client
	 * 
	 * @param message
	 *            The message to be processed (must be from FOLLOW type and must
	 *            have a payload)
	 * @throws IllegalArgumentException
	 *             Throws an exception whenever: The message hasn't a payload;
	 *             The client isn't online; The desired followed isn't online;
	 */
	private void processFollowMessage(ServerSideMessage message)
			throws IllegalArgumentException {
		// The client is asking to follow the user on the payload
		assert message.getType() == ServerSideMessage.Type.FOLLOW;
		if (message.getPayload() != null) {
			String askerNickname = message.getClientNickname();
			String followedNickname = message.getPayload();
			Client asker = getClientByNickname(askerNickname);

			// Checking if asker is online
			if (asker != null) {
				Client followed = getClientByNickname(followedNickname);
				if (followed != null) {
					asker.addFollowed(followed);
					followed.addFollower(asker);

					// Warn the followed about what happened;

					serverComm.sendNewFollower(followedNickname, askerNickname);
				} else {
					throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
				}
			} else {
				throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
			}
		} else {
			throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
		}
	}

	/**
	 * Handles the disconnection of a client (sends a message to all users)
	 * 
	 * @param message
	 *            The message to be processed (must be from CLIENT_DISCONNECTED
	 *            type)
	 * @throws IllegalArgumentException
	 *             Throws an exception whenever: The client isn't online;
	 */
	private void processClientDisconnectedMessage(ServerSideMessage message)
			throws IllegalArgumentException {
		assert message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED;
		String nickname = message.getClientNickname();

		Client clientGoing = getClientByNickname(nickname);
		// Checking if client is online
		if (clientGoing != null) {
			// Delete client from the online client list
			onlineClientsList.remove(clientGoing);

			// Warn the all the users about the disconnection
			for (Client client : onlineClientsList) {
				String clientNickname = client.getNickname();

				client.removeFollowed(clientGoing);
				client.removeFollower(clientGoing);
				serverComm.sendClientDisconnected(clientNickname, nickname);
			}
		} else {
			throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
		}
	}

	/**
	 * Handles the connection of a new client
	 * 
	 * @param message
	 *            The message to be processed (must be from CLIENT_CONNECTED
	 *            type)
	 */
	private void processClientConnectedMessage(ServerSideMessage message) {
		assert message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED;

		String nickname = message.getClientNickname();

		boolean nicknameIsAvailable = checkNicknameAvailability(nickname);

		// Checking if the choosen nickname is available, that is, if it isn't
		// already being used
		if (nicknameIsAvailable) {
			Client justArrived = new Client(nickname);

			// 1) Warn every one that there is a new client
			// 2) And send the list of online users to the new client

			for (Client client : onlineClientsList) {
				serverComm.sendClientConnected(client.getNickname(), nickname);
				serverComm.sendClientConnected(nickname, client.getNickname());
			}

			// Add the client to the online clients list
			onlineClientsList.add(justArrived);

		} else {
			serverComm.sendError(nickname, ERROR_DUPLICATED_NICKNAME);
			serverComm.disconnectClient(nickname);
		}
	}

	/**
	 * Checks if the nickname is available, that is, if the nickname isn't
	 * already being used
	 * 
	 * @param nickname
	 * @return
	 */
	private boolean checkNicknameAvailability(String nickname) {
		// If there isn't a client with the specified nickname, it's available
		return getClientByNickname(nickname) == null;
	}

	/**
	 * Sends the received Birdsong message to all the followers
	 * 
	 * @param message
	 *            The message to be processed (must be from BIRDSONG_MESSAGE
	 *            type and must have a payload)
	 * @throws IllegalArgumentException
	 *             Throws an exception whenever: The message hasn't a payload;
	 *             The client isn't online;
	 */
	private void processBirdSongMessage(ServerSideMessage message)
			throws IllegalArgumentException {

		assert message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE;

		String nickname = message.getClientNickname();
		String birdsongMessage = message.getPayload();
		// Check if birdsongMessade has content
		if (birdsongMessage != null) {
			// Get the client

			Client client = getClientByNickname(nickname);

			// Checking if client is online

			if (client != null) {
				// Get the followersList

				LinkedList<Client> followersList = client.getFollowersList();

				// Send the message to all the followers

				for (Client follower : followersList) {
					String followerNickname = follower.getNickname();
					serverComm.sendBirdsongMessage(nickname, followerNickname,
							birdsongMessage);
				}
			} else {
				throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
			}
		} else {
			throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
		}
	}

	/**
	 * Searches and returns the online client with the given nickname
	 * 
	 * @param nickname
	 * @return Null if the client isn't online. Returns the online client with
	 *         the given nickname
	 */

	private Client getClientByNickname(String nickname) {

		Client foundClient = null;
		Iterator<Client> iterator = onlineClientsList.iterator();
		boolean found = false;
		boolean ended = !iterator.hasNext();

		while (!found && !ended) {
			Client client = iterator.next();
			if (client.getNickname().equals(nickname)) {
				found = true;
				foundClient = client;
			} else {
				if (!iterator.hasNext()) {
					ended = true;
				}
			}
		}

		return foundClient;
	}

	/**
	 * Gives access to the Error Log
	 * 
	 * @return errorLog An array of Error description
	 */
	public ArrayList<String> getErrorLog() {
		return errorLog;
	}

	/**
	 * Returns the error count during the last execution
	 * 
	 * @return numberOfErrors the number of errors that occurred during the last
	 *         execution
	 */
	public int getNumberOfErrors() {
		return numberOfErrors;
	}

	public static void main(String[] args) {
		ServerComm serverComm = new ServerCommunicator();
		BirdsongServer server = new BirdsongServerImplemented();
		server.start(serverComm);
	}
}
